package wheel;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map.Entry;

import wheel.Game.ContestantAction;

/**
 * Computer is a subclass of Contestant.
 * 
 * The properties of this class represent:
 *    - information about the game/round that the Computer is allowed to know;
 *    - some assumed information, such as the frequency order of letters;
 *    - 
 * @author 		Jack Myers and Steve Gennaoui
 * @version		2.0
 *
 */
public abstract class Computer extends Contestant {
	
	/* Pat Sajak will announce (set) the current Puzzle.  The Computer needs access
	 * to it so it can remember details such as the current guessed letters and 
	 * phrases, the "answer" as it currently exists (with * for unguessed letters),
	 * whether all the consonants are guessed, etc.
	 */	
	protected Puzzle currentPuzzle;
	
	/* The Puzzle class has an ArrayList of words that represent the actual words
	 * of the puzzle.  The Computer will maintain its own list of words based on
	 * the Puzzle's answer.  This list of words has asterisks for unguessed letters
	 */
	protected ArrayList<Word> words = new ArrayList<Word>();
	
	/* The property regexp is maintained by the Computer as players take their turns.
	 * It will be updated based on the letters that have been previously guessed.
	 * As each letter is guessed, it will be removed from the regular expression.
	 * 
	 * This is a regular expression fragment representing unguessed letters that
	 * need to be searched.  It will be substituted for "*" into a full regular 
	 * expression for each word of the puzzle.
	 */
	protected String regexp;
	
	/* The following properties represent  consonants and vowels in frequency order */
	public static String consonantsInOrder = "TNSHRDLCMWFGYPBVKJXQZ";
	public static String vowelsInOrder = "EAIOU";
	
	/* Based on pattern matching against the database, the Computer will keep track
	 * of the total number of possible words it could use to solve the puzzle.  If
	 * the total number of possible words is equal to the number of words in the 
	 * puzzle, one of two facts is true:
	 * 
	 *   - The puzzle is solved, and the Computer knows the correct words to used
	 *   
	 *   - The Computer has no idea of at least one word in the puzzle, and for each
	 *     word it is clueless about, there exists another word in the puzzle with an
	 *     extra possible solution.
	 *     
	 * This is useful in establishing how close the Computer is to winning.
	 */
	protected int totalWordPossibilities;
	
	/* The Computer keeps track of the letters guessed the LAST time it ran a database
	 * analysis looking for word matches.  This way, if the number of letters guessed
	 * has not changed, it will not have to perform an expensive but redundant database
	 * search.  Initialized to an invalid value, -1.
	 */
	protected int lettersGuessedWhenLastAnalyzed = -1;
	
	/* A variable to assist in the recursive function formulateSoltion().  That function 
	 * might suggest the same solution that has been previously guessed. Second choice
	 * words will be utilized until a novel solution is suggested by the algorithm.  If
	 * such a novel solution is found, word substitution will no longer be required and
	 * substitutionRequired flag will be set to false.
	 * 
	 *  Initialized to false, as no solutions will be guessed upon Computer instantiation.
	 */
	protected Boolean substitutionRequired = false;  // If the same solution is suggested twice
	
    // The minimum number of word possibilities to guess for each word.
    // Databases are searched in order to bring back a number of possibilities per
    // word.  When should we declare we have "enough" words, and stop "lower value"
    // database searches?
    protected int perWordPossibilityThreshold;
	
	/* Connection used to communicate with MySQL database of words */
	protected Connection myConnection = JDBCMySQLConnection.getConnection();
	
	/* Enum listing all possible sources for word guesses */
	public static enum WordSource {
		LW_CAT_AND_PW,
		LW_OTHER_AND_PW,
		LW_CAT_NOT_PW,
		LW_OTHER_NOT_PW,
		POPULAR_WORDS,
		WORDNET,
		LINUX_WORDS,
		INVOKE_WORDS
	};
	
	/* Standard responses to Pat Sajak */
	protected static final String NO_SOLUTION = "\tSorry, Pat, I have no clue.";
	protected static final String SOLVE = "\t\"I'm going to try and guess the puzzle.\"";
	protected static final String SPIN = "\t\"I think I will spin the wheel.\"";
	protected static final String VOWEL = "\t\"I'd like to buy a vowel.\"";
	
	/* Debug mode toggle */
	protected boolean debugMode = false;
	
	/* Constructor */
	public Computer(String name, boolean debugMode) {
		super(name);
		this.debugMode = debugMode;
		
		ResultSet rs = null;
        Connection myConnection = null;
        Statement statement = null; 
 
        // Test database connection eagerly on Computer instantiation
        String query = "SELECT 'Hello' as hello";
        try {
            myConnection = JDBCMySQLConnection.getConnection();
            statement = myConnection.createStatement();
            rs = statement.executeQuery(query);
 
            if (rs.next()) {
                @SuppressWarnings("unused")
				String test = rs.getString("hello");

            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if (myConnection != null) {
                try {
                    myConnection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
	}


	/* The Decision Methods */
	
	@Override
	// No default behavior for major decision 
	public abstract ContestantAction getContestantAction();

	@Override
	// No default behavior for major decision
	public abstract ContestantAction getFreePlayAction();

	@Override
	// Standard method that all subclasses can use
	public char getConsonant() {
		char c = getNextConsonant();
		System.out.println("\t\"" + c + "\"");
		return c;
	}

	@Override
	// Standard method that all subclasses can use
	public char getVowel() {
		char v = getNextVowel();
		System.out.println("\t\"" + v + "\"");
		return v;
	}

	@Override
	// Calls formulateSoution
	public String getSolution() {
		// Assume word substitution not required for the first iteration
		this.substitutionRequired = false; 
		String solution = formulateSolution();
		System.out.println("\n\t\"" + solution.toUpperCase() + "\"");
		return solution;
	}
	
	/**
	 * formulateSolution() will loop through all the words of a puzzle and, using the
	 * regular expressions, come up with a list of guesses for each word.  It is a 
	 * potentially recursive function if it proposes a solution that was already guessed.
	 * 
	 * @return		The proposed solution in the form of a String.
	 */
	protected String formulateSolution() {
		//if (this.debugMode) System.out.println("*** in formulateSolution");
		
		// Initialize variables
		String solution = ""; 				// the return string
		Boolean substitutionMade = false;	// identifies if a word substitution was made
		int numberOfWordChoices = 0;		// total number of word possibilities for puzzle
		
		try {
			for (Word myWord : this.words) {
				if (this.debugMode) displayWordPossibilities(myWord);  // see which words computer is pondering
				numberOfWordChoices += myWord.getGuesses().size();  // accumulate word possibilities
				Iterator<String> it = myWord.getGuesses().keySet().iterator();	
				
				if (this.substitutionRequired 
						&& myWord.getGuesses().size() > 1 && !substitutionMade) {
					/* Substitution is required, as the top solution guess has already been 
					 * made.  Arbitrarily start substituting for the first word whose 
					 * number of possibilities is greater than 1.  Once any word has been
					 * substituted (substitutedMade == true), there is no reason to substitute
					 * further.  (This function could be called recursively.)
					 * 
					 * This is not the most intelligent way to generate a new solution.  There 
					 * is a reasonable chance that the top choice of an earlier word is correct
					 * and a later word in the phrase should really be substituted.  But it
					 * prevents the Computer from guessing the same phrase over and over.
					 */
					it.next();
					it.remove(); // Remove the word from the top of the list
					substitutionMade = true;
				}
				// The new solution is composed of the top word choice for each word.			
				if (it.hasNext()) solution += it.next() + " ";
				else {
					// No more guesses for a word, throw the exception.
					throw new IndexOutOfBoundsException();
				}
			}
			solution = solution.substring(0, solution.length()-1);  // trim last space

			if (numberOfWordChoices == this.words.size()) {
				/* The Computer has the same amount of word choices as there are words in
				 * the Puzzle.  Plus, the computer has a guess for each word.  (If any word
				 * had no suggestions, accessing the best guess, the previous block of code
				 * would throw an exception which is handled below.)
				 * 
				 * This is the best (and only) solution the Computer can postulate.  If this
				 * solution has already been guessed, return NO_SOLUTION.				 * 
				 */
				if (currentPuzzle.getPhrasesGuessed().contains(solution)) return Computer.NO_SOLUTION;
				else return solution;
			}
			else {
				// Multiple possible solutions exist
				if (currentPuzzle.getPhrasesGuessed().contains(solution)) {
					/* The "best" solution has already been guessed! The Computer needs to 
					 * recursively try again.  Substitution will be required.
					 */
					System.out.println("\t\"Hmmm..." + solution.toUpperCase() + " is guessed already.\"");
					this.substitutionRequired = true;
					solution = formulateSolution();  // recursive call

					return solution;
				}
				else {
					// The "best" solution was never guessed.  No need to recurse.
					return solution;
				}
			}
		}
		catch (IndexOutOfBoundsException e) {
			/* There is no guess for a particular word.  This exception will be thrown
			 * when the iterator has no next Guess.
			 */
			return Computer.NO_SOLUTION;  
		}
	}
	

	/**
	 * updateWordGuesses queries the database and loads possible guesses for each
	 * word into each Word's ArrayList of guesses
	 */
	   protected void updateWordGuesses() {

	        String query; 
	        this.totalWordPossibilities = 0;  // Reset the total number of possibilities
	        int i = 0;                        // Counter for the Words in the array of Words
			
			LinkedHashMap<String, Guess> guesses = new LinkedHashMap<String, Guess>();
			
	        for (Word myWord : this.words) {
	  		
	    		if (!myWord.getSolved()) {
	    				    			
	    			// Add popular words 
	    			if (guesses.size() < this.perWordPossibilityThreshold) {
		        		query =  "select distinct word from popular_words ";
		        		query += "where word regexp '" + myWord.getPattern() + "' ";
		        		query += "order by rank";
		
		        		guesses = queryDatabase("Popular Words", Computer.WordSource.POPULAR_WORDS,
		        				myWord, query, guesses);
	    			}
	    			
		            // Add words from the wordnet30.word table
	        		if (guesses.size() < this.perWordPossibilityThreshold) {

		        		query =  "select word from wordnet30.word ";
		        		query += "where word regexp '" + myWord.getPattern() + "' "; 
		        		
		        		guesses.putAll(queryDatabase("Wordnet", Computer.WordSource.WORDNET,
		        				myWord, query, guesses));
	        		}
	        		          
		            // Add words from the Linux table
	        		if (guesses.size() < this.perWordPossibilityThreshold) {

		        		query =  "select word from linux_words ";
		        		query += "where word regexp '" + myWord.getPattern() + "' "; 
		        		
		        		guesses.putAll(queryDatabase("Linux Words", Computer.WordSource.LINUX_WORDS,
		        				myWord, query, guesses));
	        		}
		            
		            // Add invoke_words as a last resort.
		            
	        		if (guesses.size() == 0) {

		        		query =  "select word from invoke_words ";
		        		query += "where word regexp '" + myWord.getPattern() + "' ";     

		        		guesses.putAll(queryDatabase("Invoke Words", 
		        				Computer.WordSource.INVOKE_WORDS, myWord, query, guesses));
		            }
	        		
	        		
		            // Assign the LinkedHashMap guesses to the word of the puzzle
		            this.words.get(i++).setGuesses(guesses);
		            guesses.clear();
	    		}
	    		else {
	    			// The word is solved, the Computer need not guess.
	    			LinkedHashMap<String, Guess> lhm = new LinkedHashMap<String, Guess>();
	    			lhm.put(myWord.getText(), new Guess(myWord.getText()));
	    			this.words.get(i++).setGuesses(lhm);
	    		}
	        }
		}
	    
	    /**
	     * 
	     */
	    private LinkedHashMap<String, Guess> queryDatabase(String queryName, 
	    			Computer.WordSource wordSource, Word myWord, String query, 
	    			LinkedHashMap<String, Guess> guesses) {
			ResultSet rs = null;
	        Statement statement;
	       
			if (this.debugMode) {
	    		/*System.out.println();
				System.out.println(guesses.size() + " possibilities for " + myWord.getText());
				for (String s : guesses) System.out.print(s + " "); */
	    		System.out.println();
				System.out.print("*** Querying " + queryName + "...");
			}	
			try {
				statement = myConnection.createStatement();
				rs = statement.executeQuery(query);
		        while (rs.next() && guesses.size() < this.perWordPossibilityThreshold) {
	            	// Check to see if word has been added to map
	            	if (!guesses.containsKey(rs.getString("word"))) {
		            	Guess myGuess = new Guess(rs.getString("word"), wordSource);    
		                guesses.put(rs.getString("word"), myGuess);  // First time in, a put will work
		                this.totalWordPossibilities++;
	            	}
	            	else {
	            		Guess previousGuess = guesses.get(rs.getString("word"));
	            		previousGuess.getSources().add(wordSource);	            		
	            	}
	            }
	            if (this.debugMode) {
	            	System.out.println("done."); 
	    			System.out.println(guesses.size() + " possibilities for " + myWord.getText());
	    			for (Entry<String, Guess> entry : myWord.getGuesses().entrySet()) {
	    				System.out.print(entry.getKey().toLowerCase() + " ");
	    			}
	            }
				
			} catch (SQLException e) {
				e.printStackTrace();
			}
			return guesses;
	    }
	
	/**
	 * prepareToSolve is invoked when the computer decides that solving the puzzle may
	 * be an option.  This method invokes several steps
	 * @return
	 */
	protected boolean prepareToSolve() {
		try {
			/* You only need to prepareToSolve if the puzzle's state has changed, 
			 * i.e., if more letters have been guessed.
			 */
			if (currentPuzzle.getGuesses().length() != lettersGuessedWhenLastAnalyzed) {
				updateRegexp();       // update the reg expressions to removed guessed (lower case) letters
				loadWords();          // load the words based on the puzzle's current answer
				updateWords();        // update each word's text to show correctly guessed letters and 
									  //     pattern to use new regexp	
				updateWordGuesses();  // update each word's guess by using the regexp against the database
		    	if (this.debugMode) for (Word myWord : this.words) displayWordPossibilities(myWord);
			}
			return true;
			
		}
		catch (Exception e) {
			System.out.println("\t\"Having trouble with data access.\"");
			return false;
		}
	}
	
	/**
	 * getNextConsonant is a basic algorithm that gets the most frequent consonant that
	 * hasn't been guessed.
	 * 
	 * @return		the "best" consonant
	 */
	protected char getNextConsonant() {
		for (char c : AIagent3.consonantsInOrder.toCharArray()) {
			if (this.currentPuzzle.getGuesses().indexOf(c) == -1) {
				return c;
			}
		}
		return 0;
	}
	
	/**
	 * getNextVowel is a basic algorithm that gets the most frequent vowel that
	 * hasn't been guessed.
	 * 
	 * @return		the "best" consonant
	 */
	protected char getNextVowel() {
		for (char v : AIagent3.vowelsInOrder.toCharArray()) {
			if (this.currentPuzzle.getGuesses().indexOf(v) == -1) {				
				return v;
			}
		}
		return 0;
	}

	
	/* Utility Functions */
	
	/**
	 * loadWords maintains the Computer's word list based on the Puzzle answer 
	 * (which contains * characters).
	 */
	protected void loadWords() {
		// Clear the ArrayList words and get the current Puzzle answer.
		this.words.clear();
		String answer = this.currentPuzzle.getAnswer().toLowerCase();
		
		// Split treats * as delimiters; therefore convert * to Q so split will work.
		String[] terms = answer.replace("*", "Q").split(Game.SPLITTERS);
		for (int i = 0; i < terms.length; i++) {
			// Now convert the Q back to an *
			this.words.add(new Word(i,terms[i].replace("Q", "*")));
		}
		// this.words is now updated.
	}
	
	/**
	 * updateWords will modify the list of words for the current puzzle for use in
	 * a database query. 
	 * 
	 * The ArrayList words is initialized by loadWords() based on the current puzzle
	 * answer which has asterisks for unguessed letters.  Each * needs to be 
	 * converted into a regular expression that represents the still unknown letters
	 * (this.regexp).  
	 */
	public void updateWords() {
		String fullRegexp; // Holds the FULL regexp for each word  

		for (Word word : words) {
			// For each word, build up the regular expression
			fullRegexp = "^";
			fullRegexp += word.getText().replace("*", this.regexp);  // Update the text 
			fullRegexp += "$";
			fullRegexp = fullRegexp.replace("'", "''");
						
			if (word.getText().indexOf('*') == -1) word.setSolved(true);

			word.setPattern(fullRegexp);  // Update the pattern in the Word object
		}
		
		// Update the fact that analysis has been completed for the number of guessed letters.
		lettersGuessedWhenLastAnalyzed = currentPuzzle.getGuesses().length();
	}
	
	/**
	 * updateRegexp removes guessed letters from the regexp component
	 */
	protected void updateRegexp() {
		String eliminatedLetters = this.currentPuzzle.getGuesses().toLowerCase();
		if (!currentPuzzle.isMoreConsonants()) eliminatedLetters += consonantsInOrder.toLowerCase();
		if (!currentPuzzle.isMoreVowels()) eliminatedLetters += vowelsInOrder.toLowerCase();
		
		for (Character c : eliminatedLetters.toCharArray()) {
			this.regexp = this.regexp.replace(c.toString(), "");
		}
	}
	
	/**
	 * displayWordPossibilities will give insight into how many guesses a Computer 
	 * has for a given word.  If the number of guesses is 50 or less, the guesses will
	 * be displayed in order.
	 * 
	 * @param myWord	the Word for which one wants to see the possibility data
	 */
	protected void displayWordPossibilities(Word myWord) {
		System.out.print("\n\t(" + myWord.getGuesses().size() + " guesses for " + myWord.getText() + ") ");
		if (myWord.getGuesses().size() <= 50) {
			for (Entry<String, Guess> entry : myWord.getGuesses().entrySet()) {
				System.out.print(entry.getKey().toLowerCase() + " ");
			}
		}
		System.out.println();
	}
	
	/**
	 * listDatabaseTables is used only in debugging to ensure that the correct collection
	 * of tables and views is available to any Computer object.
	 */
	protected static void listDatabaseTables() {

        try {
    		ResultSet rs = null;
            Connection myConnection = JDBCMySQLConnection.getConnection();
            Statement myStatement = myConnection.createStatement();; 

			System.out.println();
            String query =  
    	    		"select table_name from information_schema.tables where table_schema = 'wordnet30'";
			rs = myStatement.executeQuery(query);  
	        while (rs.next()) {
	            System.out.print(rs.getString("table_name") + " ");
	        }
	        
			System.out.println();
    	    query =  
    	    		"select word from wordnet30.word where word = 'forgive';";
			rs = myStatement.executeQuery(query);    
	        while (rs.next()) {
	            System.out.print(rs.getString("word") + " ");
	        }
			System.out.println();
	        
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} 
	}
 
	
	// Getters and Setters

	public ArrayList<Word> getWords() {
		return words;
	}

	public void setWords(ArrayList<Word> words) {
		this.words = words;
	}


	public Puzzle getCurrentPuzzle() {
		return currentPuzzle;
	}

	public void setCurrentPuzzle(Puzzle currentPuzzle) {
		this.currentPuzzle = currentPuzzle;
		loadWords();
		this.regexp = "[abcdefghijklmnopqrstuvwxyz]";
	}
	
	public abstract void learnPuzzle(Puzzle puzzle);


	public boolean isDebugMode() {
		return debugMode;
	}


	public void setDebugMode(boolean debugMode) {
		this.debugMode = debugMode;
	}
	
	
}
